/*
 * The MIT License
 *
 * Copyright 2012-2014 Zafar Khaja (zafarkhaja@gmail.com).
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package com.github.zafarkhaja.semver;

import java.util.Arrays;

/**
 * The {@code MetadataVersion} class is used to represent the pre-release version and the build
 * metadata.
 *
 * @author Zafar Khaja (zafarkhaja@gmail.com)
 * @since 0.2.0
 */
class MetadataVersion implements Comparable<MetadataVersion> {

  /**
   * Null metadata, the implementation of the Null Object design pattern.
   */
  static final MetadataVersion NULL = new NullMetadataVersion();

  /**
   * The implementation of the Null Object design pattern.
   */
  private static class NullMetadataVersion extends MetadataVersion {

    /**
     * Constructs a {@code NullMetadataVersion} instance.
     */
    public NullMetadataVersion() {
      super(null);
    }

    /**
     * {@inheritDoc}.
     * 
     * @throws NullPointerException
     *           as Null metadata cannot be incremented
     */
    @Override
    MetadataVersion increment() {
      throw new NullPointerException("Metadata version is NULL");
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    public String toString() {
      return "";
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    public int hashCode() {
      return 0;
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    public boolean equals(Object other) {
      return other instanceof NullMetadataVersion;
    }

    /**
     * {@inheritDoc}.
     */
    @Override
    public int compareTo(MetadataVersion other) {
      if (!equals(other)) {
        /**
         * Pre-release versions have a lower precedence than the associated normal version. (SemVer
         * p.9)
         */
        return 1;
      }
      return 0;
    }
  }

  /**
   * The array containing the version's identifiers.
   */
  private final String[] idents;

  /**
   * Constructs a {@code MetadataVersion} instance with identifiers.
   * 
   * @param identifiers
   *          the version's identifiers
   */
  MetadataVersion(String[] identifiers) {
    idents = identifiers;
  }

  /**
   * Increments the metadata version.
   *
   * @return a new instance of the {@code MetadataVersion} class
   */
  MetadataVersion increment() {
    String[] ids = idents;
    String lastId = ids[ids.length - 1];
    if (isInt(lastId)) {
      int intId = Integer.parseInt(lastId);
      ids[ids.length - 1] = String.valueOf(++intId);
    } else {
      ids = Arrays.copyOf(ids, ids.length + 1);
      ids[ids.length - 1] = String.valueOf(1);
    }
    return new MetadataVersion(ids);
  }

  /**
   * {@inheritDoc}.
   */
  @Override
  public boolean equals(Object other) {
    if (this == other) {
      return true;
    }
    if (!(other instanceof MetadataVersion)) {
      return false;
    }
    return compareTo((MetadataVersion) other) == 0;
  }

  /**
   * {@inheritDoc}.
   */
  @Override
  public int hashCode() {
    return Arrays.hashCode(idents);
  }

  /**
   * {@inheritDoc}.
   */
  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    for (String ident : idents) {
      sb.append(ident).append(".");
    }
    return sb.deleteCharAt(sb.lastIndexOf(".")).toString();
  }

  /**
   * {@inheritDoc}.
   */
  @Override
  public int compareTo(MetadataVersion other) {
    if (other == MetadataVersion.NULL) {
      /**
       * Pre-release versions have a lower precedence than the associated normal version. (SemVer
       * p.9)
       */
      return -1;
    }
    int result = compareIdentifierArrays(other.idents);
    if (result == 0) {
      /**
       * A larger set of pre-release fields has a higher precedence than a smaller set, if all of
       * the preceding identifiers are equal. (SemVer p.11)
       */
      result = idents.length - other.idents.length;
    }
    return result;
  }

  /**
   * Compares two arrays of identifiers.
   *
   * @param otherIdents
   *          the identifiers of the other version
   * @return integer result of comparison compatible with the {@code Comparable.compareTo} method
   */
  private int compareIdentifierArrays(String[] otherIdents) {
    int result = 0;
    int length = getLeastCommonArrayLength(idents, otherIdents);
    for (int i = 0; i < length; i++) {
      result = compareIdentifiers(idents[i], otherIdents[i]);
      if (result != 0) {
        break;
      }
    }
    return result;
  }

  /**
   * Returns the size of the smallest array.
   *
   * @param arr1
   *          the first array
   * @param arr2
   *          the second array
   * @return the size of the smallest array
   */
  private int getLeastCommonArrayLength(String[] arr1, String[] arr2) {
    return arr1.length <= arr2.length ? arr1.length : arr2.length;
  }

  /**
   * Compares two identifiers.
   *
   * @param ident1
   *          the first identifier
   * @param ident2
   *          the second identifier
   * @return integer result of comparison compatible with the {@code Comparable.compareTo} method
   */
  private int compareIdentifiers(String ident1, String ident2) {
    if (isInt(ident1) && isInt(ident2)) {
      return Integer.parseInt(ident1) - Integer.parseInt(ident2);
    } else {
      return ident1.compareTo(ident2);
    }
  }

  /**
   * Checks if the specified string is an integer.
   *
   * @param str
   *          the string to check
   * @return {@code true} if the specified string is an integer or {@code false} otherwise
   */
  private boolean isInt(String str) {
    try {
      Integer.parseInt(str);
    } catch (NumberFormatException e) {
      return false;
    }
    return true;
  }
}
